/**
 * 
 */
package tools;

import org.apache.commons.math3.analysis.polynomials.PolynomialFunction;
import org.apache.commons.math3.linear.Array2DRowRealMatrix;
import org.apache.commons.math3.linear.ArrayRealVector;
import org.apache.commons.math3.linear.DecompositionSolver;
import org.apache.commons.math3.linear.LUDecomposition;
import org.apache.commons.math3.linear.RealMatrix;
import org.apache.commons.math3.linear.RealVector;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

import interfaces.ILinearEquation;

/**
 * @author vks2
 * 
 */
public class PolynomialGenerator implements ILinearEquation {

	private double[][] xy;
	public RealVector constants;
	public RealMatrix coefficients;
	public RealVector solution;
	public PolynomialFunction[] poly;
	public int degreeOfPolynomial;
	public int differentailOrder;
	public int totalNDPoints;

	public PolynomialGenerator(double[][] xy) {
		this.xy = xy;

		this.differentailOrder = this.xy[0].length - 1;
		this.totalNDPoints = this.xy.length;
		degreeOfPolynomial = this.computeDegree();

		coefficients = new Array2DRowRealMatrix(this.degreeOfPolynomial,
				this.degreeOfPolynomial);
		constants = new ArrayRealVector(this.degreeOfPolynomial);
		poly = new PolynomialFunction[this.differentailOrder];
		this.createDifferentialEquations();
		this.createCoefficients();
		this.createConstants();
	}

	/**
	 * Returns that degree of polynomial required.
	 * 
	 */
	private int computeDegree() {
		return this.differentailOrder * this.totalNDPoints; // As an n-1 degree
															// polynomial can
															// fit n input
															// points
	}

	/**
	 * Creates the series of polynomial equations that can be later used to
	 * create linear equations
	 */
	private void createDifferentialEquations() {
		double[] coef = new double[degreeOfPolynomial];

		for (int i = 0; i < coef.length; i++)
			coef[i] = 1;

		poly[0] = new PolynomialFunction(coef);

		for (int n = 0; n < this.differentailOrder; n++) {
			poly[n] = getDifferentiated(poly[0], n % this.differentailOrder);
		}

	}

	/**
	 * @param polynomialFunction
	 * @param nThDerivative
	 * @return
	 */
	private PolynomialFunction getDifferentiated(
			PolynomialFunction polynomialFunction, int nThDerivative) {

		for (int n = 0; n < nThDerivative; n++) {
			polynomialFunction = (PolynomialFunction) polynomialFunction
					.derivative();
		}
		return polynomialFunction;
	}

	/**
	 * Creates the coefficient matrix
	 */
	private void createCoefficients() {
		int counter = 0;
		for (int totalEqCounter = 0; totalEqCounter < this.totalNDPoints; totalEqCounter++) {
			for (int totalRowCounter = 0; totalRowCounter < this.differentailOrder; totalRowCounter++) {
				double[] row = new double[degreeOfPolynomial];
				double[] polynomialCoeff = poly[totalRowCounter]
						.getCoefficients();
				for (int q = 0; q < degreeOfPolynomial; q++) {
					if (q >= (degreeOfPolynomial - polynomialCoeff.length)) {
						row[q] = polynomialCoeff[q - degreeOfPolynomial + polynomialCoeff.length]
								* Math.pow(xy[totalEqCounter][0], (q - (degreeOfPolynomial - polynomialCoeff.length)));
					} else {
						row[q] = 0;
					}
				}
				coefficients.setRow(counter, row);
				counter++;
			}
		}
	}

	/**
	 * Creates the constant matrix
	 */
	private void createConstants() {
		int counter = 0;
		for (int n = 0; n < xy.length; n++) {
			for (int l = 1; l < xy[n].length; l++) {
				constants.setEntry(counter, xy[n][l]);
				counter++;
			}
		}
	}

	@Override
	public void solveEquation() {
		DecompositionSolver solver = new LUDecomposition(coefficients,
				0.000000000000000001).getSolver();
		solution = solver.solve(constants);
	}

	@Override
	public double[][] getPredicted(int countOfPredict) {
		PolynomialFunction polyEquation = new PolynomialFunction(
				solution.toArray());
		double[][] pred_val = new double[this.totalNDPoints + countOfPredict][2];

		for (int n = 0; n < this.totalNDPoints - 1; n++) {
			pred_val[n][0] = this.xy[n][0];
			pred_val[n][1] = this.xy[n][1];
		}

		double[] intermediate = getIntermediatePoints(
				this.xy[this.totalNDPoints - 2][0],
				this.xy[this.totalNDPoints - 1][0], countOfPredict);

		for (int f = this.totalNDPoints - 1; f < this.totalNDPoints
				+ countOfPredict - 1; f++) {
			pred_val[f][0] = intermediate[f - this.totalNDPoints + 1];
			pred_val[f][1] = polyEquation.value(pred_val[f][0]);
		}

		pred_val[pred_val.length - 1][0] = this.xy[this.totalNDPoints - 1][0];
		pred_val[pred_val.length - 1][1] = this.xy[this.totalNDPoints - 1][1];

		return pred_val;
	}

	/**
	 * 
	 * This function gets that points between two points. Private function will
	 * be used in to get the predicted points after the polynomial equation is
	 * solved.
	 * 
	 * @param x1
	 * @param x2
	 * @param countOfPredict
	 * @return
	 */
	private double[] getIntermediatePoints(double startPoint, double endPoint,
			int countOfPredict) {

		int parts = countOfPredict + 1;
		double[] predictedPoints = new double[countOfPredict];

		for (int countPoints = 1; countPoints < parts; countPoints++) {
			predictedPoints[countPoints - 1] = startPoint + countPoints
					* ((endPoint - startPoint) / parts);
		}
		return predictedPoints;
	}
}
